package com.gitee.l0km.casban;

import java.io.IOException;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.concurrent.TimeUnit;

import com.gitee.l0km.aocache.annotations.AoWeakCacheable;
import com.gitee.l0km.casban.exception.PackageScanException;
import com.gitee.l0km.com4j.basex.WeakSupplier;
import com.google.common.base.CharMatcher;
import com.google.common.base.Function;
import com.google.common.base.Functions;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.base.Supplier;
import com.google.common.collect.FluentIterable;
import com.google.common.reflect.ClassPath;
import com.google.common.reflect.ClassPath.ClassInfo;

import static com.gitee.l0km.com4j.base.SimpleLog.log;
import static com.google.common.base.Preconditions.checkNotNull;

/**
 * 基于GUAVA实现的类扫描工具，用于扫描指定包路径下的所有类。
 * <p>
 * 主要功能：
 * <ul>
 *   <li>扫描指定包名下的所有类</li>
 *   <li>支持包名过滤和类过滤</li>
 *   <li>支持调试输出</li>
 *   <li>使用弱引用缓存提高性能</li>
 * </ul>
 *
 * <p>
 * 使用示例：
 * <pre>
 * {@code
 *   // 扫描com.example包下的所有类
 *   PackageScanner scanner = new PackageScanner();
 *   Set<Class<?>> classes = scanner.getClasses("com.example");
 *
 *   // 扫描时排除某些包
 *   Predicate<String> excludeFilter = pkg -> pkg.startsWith("com.example.internal");
 *   classes = scanner.getClasses("com.example", excludeFilter);
 * }
 * </pre>
 * </p>
 *
 * <p>
 * 注意事项：
 * <ul>
 *   <li>线程安全：本类是线程安全的，可以安全地在多线程环境下使用</li>
 *   <li>性能：首次扫描会较慢，后续扫描会使用缓存提高性能</li>
 *   <li>内存：使用弱引用缓存，不会导致内存泄漏</li>
 * </ul>
 *
 * @author guyadong
 *
 */
public class PackageScanner {
	public static final PackageScanner JDK_PACKAGE_SCANNER = new PackageScanner();
	protected static final CharMatcher FILE_SEPARATOR_MATCHER = CharMatcher.anyOf("/\\");
	private static final Function<ClassInfo, String> CLASS_INFO_NAME_FUN = new Function<ClassInfo, String>(){
		@Override
		public String apply(ClassInfo input) {
			return input.getName();
		}};
	protected final ClassLoader classLoader;
	protected final Function<String, Class<?>> CLASS_FORNAME_GET_FUN = new Function<String,Class<?>>() {
		@Override
		public Class<?> apply(String className) {
			return classFormName(className);
		}
	};
	@AoWeakCacheable(expireAfterAccess = 60, expireAfterAccessTimeUnit = TimeUnit.SECONDS)
	protected Class<?> classFormName(String className){
		try {
			return Class.forName(className, false, classLoader);
		} catch (Throwable e) {
			if(debugOutout) {
				log("{}:{}",e.getClass().getSimpleName(), e.getMessage());
			}
			return null;
		}
	}
	private final WeakSupplier<ClassPath>  classpath = WeakSupplier.of(new Supplier<ClassPath>() {
		@Override
		public ClassPath get() {
			try {
				return ClassPath.from(classLoader);
			} catch (IOException e) {
				throw new PackageScanException(e);
			}
		}},false);
	protected boolean debugOutout = false;
	public PackageScanner() {
		this.classLoader = getDefaultClassLoader();
	}

    public void setDebugOutout(boolean debugOutout) {
        this.debugOutout = debugOutout;
    }

	/**
	 * 扫描指定包名下的所有类
	 * @param pkg
	 * @param excludedPackageFilter 
	 * @param classFilters
	 */
	@SuppressWarnings("unchecked")
	public final Set<Class<?>> getClasses(Package pkg, Predicate<String> excludedPackageFilter, Predicate<Class<?>>... classFilters) {
		return getClasses(checkNotNull(pkg,"pkg is null").getName(), CountLimitStrategy.ANY, excludedPackageFilter, classFilters);
	}

	/**
	 * 扫描指定包名下的所有类
	 * @param packagename
	 * @param finisher 
	 * @param excludedPackageFilter
	 * @param classFilters
	 */
	@SafeVarargs
	public final Set<Class<?>> getClasses(String packagename, 
			Supplier<RangeFilter> finisher, 
			Predicate<String> excludedPackageFilter, Predicate<Class<?>>... classFilters) {
		try {
			LinkedHashSet<Class<?>> classes = getClasses(packagename,excludedPackageFilter);
			return CasbanScanners.findClasses(classes,finisher,classFilters);
		} catch (IOException e) {
			throw new PackageScanException(e);
		}
	}
	@SafeVarargs
	public final Class<?> getClass(boolean onlyOne,String packagename, Predicate<String> excludedPackageFilter, Predicate<Class<?>>... classFilters) {
		Set<Class<?>> classes = getClasses(packagename,CountLimitStrategy.ANY,excludedPackageFilter, classFilters);
		return CasbanScanners.fetchClassOrnull(classes,onlyOne, "class");
	}
	protected static Predicate<String> buildPackageFilter(String packagename, Predicate<String>excludePackageFilter){
		Predicate<String> packageFilter = CommonPredicates.basePackageFilter(packagename);
		if(null != excludePackageFilter) {
			packageFilter = Predicates.and(
					CommonPredicates.basePackageFilter(packagename),
					Predicates.not(excludePackageFilter));
		}
		return packageFilter;
	}
	/**
	 * 扫描指定包名下的所有类
	 * @param packagename
	 * @param excludePackageFilter 排除包名过滤器
	 * @throws IOException 
	 */
	protected LinkedHashSet<Class<?>> getClasses(String packagename, Predicate<String>excludePackageFilter) throws IOException {
		checkNotNull(packagename, "packagename must not be null");
		Predicate<String> packageFilter = buildPackageFilter(packagename,excludePackageFilter);
		Predicate<ClassInfo> classInfoFilter = Predicates.compose(
				packageFilter, 
				CLASS_INFO_NAME_FUN);
		Function<ClassInfo, Class<?>> classInfoFun = Functions.compose(
				CLASS_FORNAME_GET_FUN, 
				CLASS_INFO_NAME_FUN);
		LinkedHashSet<Class<?>> classes = FluentIterable.from(classpath.get().getResources())
				.filter(ClassInfo.class)
				.filter(classInfoFilter)
				.transform(classInfoFun)
				.filter(Predicates.notNull())
				.copyInto(new LinkedHashSet<Class<?>>());
		if(debugOutout) {
			log("{} classes loaded",classes.size());
		}
		return classes;
	}
	/**
	 * Return the default ClassLoader to use: typically the thread context
	 * ClassLoader, if available; the ClassLoader that loaded the ClassUtils
	 * class will be used as fallback.
	 * <p>Call this method if you intend to use the thread context ClassLoader
	 * in a scenario where you clearly prefer a non-null ClassLoader reference:
	 * for example, for class path resource loading (but not necessarily for
	 * {@code Class.forName}, which accepts a {@code null} ClassLoader
	 * reference as well).
	 * @return the default ClassLoader (only {@code null} if even the system
	 * ClassLoader isn't accessible)
	 * @see Thread#getContextClassLoader()
	 * @see ClassLoader#getSystemClassLoader()
	 */
	private static ClassLoader getDefaultClassLoader() {
		ClassLoader cl = null;
		try {
			cl = Thread.currentThread().getContextClassLoader();
		}
		catch (Throwable ex) {
			// Cannot access thread context ClassLoader - falling back...
		}
		if (cl == null) {
			// No thread context class loader -> use class loader of this class.
			cl = PackageScanner.class.getClassLoader();
			if (cl == null) {
				// getClassLoader() returning null indicates the bootstrap ClassLoader
				try {
					cl = ClassLoader.getSystemClassLoader();
				}
				catch (Throwable ex) {
					// Cannot access system ClassLoader - oh well, maybe the caller can live with null...
				}
			}
		}
		return cl;
	}
}
